import axios from "axios";
import "colors";
import * as program from "commander";
import * as fs from "fs";
import * as hbs from "handlebars";
import * as inquirer from "inquirer";
import * as ora from "ora";
import * as os from "os";
import * as path from "path";
import * as rimraf from "rimraf";
import * as util from "util";
import { CfgStorage } from "../../cfg";
import * as npm from "../../npm";
import {
  ITask,
  resumeConsoleLog,
  suspendConsoleLog,
  task,
  TaskChain,
  tryResumeConsoleLog
} from "../../util";
import { Manifest } from "./manifest";

const tempfile = require("tempfile");
const copy = require("recursive-copy");
const tar = require("tar");
const walk = require("walk");
const boxen = require("boxen");

const readFile = util.promisify<string, string, string>(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const mkdir = util.promisify(fs.mkdir);
const rmdir = util.promisify(rimraf);

export const create = async (pkgName: string) => {
  await new ProjCreator(pkgName).run();
};

export const downloadTslintCfg = async (
  projPath: string,
  projId: string,
  gitlabApi: string,
  gitlabToken: string,
  cfgPath: string
) => {
  const resp = await axios.request({
    method: "get",
    url: `${gitlabApi}/projects/${projId}/repository/files`,
    params: {
      ref: "master",
      file_path: cfgPath
    },
    headers: {
      "PRIVATE-TOKEN": gitlabToken
    },
    timeout: 5000
  });
  await writeFile(path.resolve(projPath, "tslint.json"), Buffer.from(resp.data.content, "base64"));
};

export const downloadDllCfg = async () => {
  const cfg = CfgStorage.getInstance();
  const projId = cfg.get("xwidget.create.cfgProj");
  const api = cfg.get("gitlab.api");
  const token = cfg.get("gitlab.privateToken");
  const cfgPath = cfg.get("xwidget.create.dllCfgFilePath");

  try {
    const resp = await axios.request({
      method: "get",
      url: `${api}/projects/${projId}/repository/files`,
      params: {
        ref: "master",
        file_path: cfgPath
      },
      headers: {
        "PRIVATE-TOKEN": token
      },
      timeout: 5000
    });
    return JSON.parse(Buffer.from(resp.data.content, "base64").toString());
  } catch (e) {
    throw new Error(`下载 DLL 配置失败: ${e.message}`);
  }
};

export const makeErrMsg = (...lines: string[]) => lines.join("\n");

export const assertConfigIsSet = async (keys: string[]) => {
  const cfg = CfgStorage.getInstance();
  let goon = true;
  let missingKey: string = "";
  keys.every(k => {
    goon = !!cfg.get(k);
    if (!goon) missingKey = k;
    return goon;
  });
  if (missingKey !== "") {
    const msg = makeErrMsg(
      "缺少配置项，请通过下面的命令来设定",
      `xwidget config set ${missingKey.bold} ${"your-custom-value".bold}`
    );
    throw new Error(msg);
  }
};

export class ProjCreator extends TaskChain {
  private pkgName: string;
  private fullPkgName: string;
  private projPath: string;
  private shouldCleanup = true;
  private isOldWidget = false;
  private wns: string;
  private isCommon = false;
  private sysCode = "";
  private platform = "";

  constructor(pkgName: string) {
    super();
    this.pkgName = pkgName;
    this.fullPkgName = "@qianmi/" + pkgName;
    this.projPath = path.join(process.cwd(), this.pkgName);
  }

  @task("检查配置项")
  async assertConfigIsSet() {
    return assertConfigIsSet(["gitlab.privateToken"]);
  }

  @task("校验包名")
  async assertPkgName() {
    const invalidChIdx = npm.isPkgNameOK(this.pkgName);
    if (invalidChIdx.length === 0) return;

    const str: string[] = [];
    this.pkgName.split("").forEach((c, i) => {
      if (invalidChIdx.indexOf(i) !== -1) {
        str.push(c.red);
      } else {
        str.push(c);
      }
    });

    const msg = makeErrMsg("包名含有不合规的字符，请重新指定", str.join(""));
    throw new Error(msg);
  }

  @task("校验包名唯一性")
  async assertPkgNameDistinct() {
    const fullName = this.fullPkgName;
    const reg = this.cfg.get<string>("cnpm.registry");
    let isExist = false;
    try {
      isExist = await npm.isPkgExist(fullName);
    } catch (e) {
      const msg = makeErrMsg("访问仓库失败，请检查网络设置", reg);
      throw new Error(msg);
    }

    if (isExist) {
      const msg = makeErrMsg(`${reg} 中已经存在同名包 ${fullName.bold}`);
      throw new Error(msg);
    }
  }

  @task("创建项目目录")
  async createProjDir() {
    try {
      await mkdir(this.pkgName);
    } catch (e) {
      this.shouldCleanup = false;
      if (e.code === "EEXIST") {
        throw new Error(`已经存在同名文件: ${this.pkgName.bold}`);
      }
      throw e;
    }
  }

  @task("生成项目模板")
  async processBoilerplate() {
    await this.downloadBoilerplate();
    await this.downloadTslintCfg();

    const dll = await this.downloadDllCfg();
    const pkgs: string[] = [];
    Object.keys(dll).forEach(k => {
      const v = dll[k][0];
      if (v === "-") return;
      pkgs.push(`"${k}": "${v}"`);
    });
    await this.injectBoilerplate(this.pkgName, {
      name: this.fullPkgName,
      dll: pkgs.join(",\n")
    });
  }

  @task("是否是子组件")
  async isCommonWidget() {
    const ans: any = await inquirer.prompt({
      type: "confirm",
      name: "isCommon",
      message: "是否是子组件",
      default: false
    });
    this.isCommon = ans.isCommon;
  }

  @task("选择组件应用平台")
  async choosePlatform() {
    let ans: any = await inquirer.prompt({
      type: "list",
      name: "platform",
      message: "选择组件应用平台",
      choices: [
        {
          name: "微信",
          value: "weixin"
        },
        {
          name: "pc",
          value: "pc"
        },
        {
          name: "pad",
          value: "pad"
        },
        {
          name: "其他",
          value: "__not_set__"
        }
      ],
      default: "weixin"
    });
    this.platform = ans.platform;
    if (this.platform === "__not_set__") {
      ans = await inquirer.prompt({
        type: "input",
        name: "platform",
        message: "请输入平台代码",
        validate(input) {
          return input.trim().length > 0;
        }
      });
      this.platform = ans.platform.trim();
    }
  }

  @task("选择所属业务线")
  async chooseSysCode() {
    let ans: any = await inquirer.prompt({
      type: "list",
      name: "sysCode",
      message: "选择所属业务线",
      choices: [
        {
          name: "云商城",
          value: "d2cStore"
        },
        {
          name: "云订货",
          value: "d2p"
        },
        {
          name: "商品中心",
          value: "goodsCenter"
        },
        {
          name: "个人中心",
          value: "memberHomePage"
        },
        {
          name: "店铺",
          value: "shopHomePage"
        },
        {
          name: "收银小票",
          value: "cashRegister"
        },
        {
          name: "通用",
          value: ""
        },
        {
          name: "其他",
          value: "__not_set__"
        }
      ],
      default: "d2cStore"
    });
    this.sysCode = ans.sysCode;
    if (this.sysCode === "__not_set__") {
      ans = await inquirer.prompt({
        type: "input",
        name: "sysCode",
        message: "请输入业务线代码",
        validate(input) {
          return input.trim().length > 0;
        }
      });
      this.sysCode = ans.sysCode.trim();
    }
  }

  @task("写入 manifest")
  async writeManifest() {
    process.chdir(this.pkgName);
    Manifest.getInstance().set("name", this.fullPkgName);
    Manifest.getInstance().set("type", this.isCommon ? "sub" : "bar");
    Manifest.getInstance().set("systemCode", this.sysCode);
    Manifest.getInstance().set("platform", this.platform);
    process.chdir("..");
  }

  @task("安装项目依赖")
  async doNpmInstall() {
    process.chdir(this.pkgName);
    suspendConsoleLog();
    const { install } = await npm.bootstrap();
    await install([]);
    resumeConsoleLog();
    process.chdir("..");
  }

  async enterTask(t: ITask): Promise<void> {
    t.extra.spinner = ora(t.name);
    const skip = ["是否是子组件", "选择所属业务线", "选择组件应用平台"];
    if (skip.indexOf(t.name) === -1) t.extra.spinner.start();
  }

  async leaveTask(t: ITask): Promise<void> {
    t.extra.spinner.succeed(t.name);
  }

  async leaveTaskError(t: ITask, e: Error): Promise<void> {
    tryResumeConsoleLog();

    t.extra.spinner.stop();
    console.log("\n" + "✘ ".red + t.name + ":\n");
    console.log(
      e.message
        .split("\n")
        .map(l => "  " + l)
        .join("\n") + "\n"
    );
    if (this.shouldCleanup) {
      try {
        await rmdir(this.pkgName);
      } catch (e) {}
    }
    if (e.stack) {
      console.log(`\n${"Call Stack:".red.bold}\n` + e.stack + "\n");
    }
    console.log(
      boxen(
        "It looks like a bug? Reporting it to http://git.dev.qianmi.com/OF2764/xwidget-cli/issues",
        { borderStyle: "double-single" }
      ).yellow
    );
  }

  async done(hasFailures: boolean): Promise<void> {
    if (hasFailures) return;
    console.log(`\n🎉  项目初始化成功，你可以:\n`);
    console.log(`  启动开发: ${"cd".green} ${this.pkgName} && ${"xwidget widget".green} dev`);
    console.log(`  更多指令: ${"xwidget widget".green} -h`);
    console.log();
  }

  get cfg() {
    return CfgStorage.getInstance();
  }

  extractArchiveName(tgz: string) {
    return new Promise<string>(resolve => {
      let p = "";
      tar.t({
        file: tgz,
        onentry: (entry: any) => {
          p = entry.path;
          resolve(p.replace(new RegExp(`^${path.sep}|${path.sep}$`, "g"), ""));
        }
      });
    });
  }

  async downloadBoilerplate() {
    const projId = this.cfg.get("xwidget.create.boilerplateProj");
    const api = this.cfg.get("gitlab.api");
    const token = this.cfg.get("gitlab.privateToken");

    try {
      const resp = await axios.request({
        method: "get",
        url: `${api}/projects/${projId}/repository/archive`,
        headers: {
          "PRIVATE-TOKEN": token
        },
        responseType: "arraybuffer",
        timeout: 5000
      });
      const tf = tempfile();
      await writeFile(tf, resp.data);

      // extract to a temporary directory
      const etf = tempfile();
      await mkdir(etf);
      await tar.x({ file: tf, C: etf });

      const archName = await this.extractArchiveName(tf);
      await copy(path.join(etf, archName), this.pkgName, { dot: true });
    } catch (e) {
      throw new Error(`下载项目模板失败: ${e.message}`);
    }
  }

  async injectBoilerplate(dir: string, ctx: any) {
    const walker = walk.walk(dir);

    return new Promise((resolve, reject) => {
      walker.on("file", async (root: string, stats: any, next: any) => {
        const file = path.join(root, stats.name);
        try {
          if (stats.isFile() && /\.(json|tsx?|jsx?)$/.test(file)) {
            const src = await readFile(file, "utf8");
            const tpl = hbs.compile(src);
            await writeFile(file, tpl(ctx));
          }
          next();
        } catch (e) {
          reject(new Error(`处理模板文件 ${file} 失败: ${e.message}`));
        }
      });

      walker.on("directory", (root: string, stats: any, next: any) => {
        next();
      });

      walker.on("error", (root: string, stats: any) => {
        reject(stats.error);
      });

      walker.on("end", () => {
        resolve();
      });
    });
  }

  async downloadTslintCfg() {
    const projId = this.cfg.get<string>("xwidget.create.cfgProj");
    const api = this.cfg.get<string>("gitlab.api");
    const token = this.cfg.get<string>("gitlab.privateToken");
    const cfgPath = this.cfg.get<string>("xwidget.create.tslintCfgFilePath");

    try {
      await downloadTslintCfg(this.projPath, projId, api, token, cfgPath);
    } catch (e) {
      throw new Error(`下载 TSLint 配置失败: ${e.message}`);
    }
  }

  async downloadDllCfg() {
    return downloadDllCfg();
  }
}
